import java.util.ArrayList;

import java.awt.Color;
import java.util.Arrays;


/* This class is essentially a copy of the State.java class but it is used to simulate 
 * what different moves would mean for the agent without actually making these moves.
 * This would ideally not be defined in this file like this but the restrictions of the project
 * prohibits us from doing anything else.
 * 
 * For examining the code that we have written, you can ignore the first 300 (more or less) lines of this file
 * (all the way down to "abstract class Feature")
 */
 class SimulationState {
	public static final int COLS = 10;
	public static final int ROWS = 21;
	public static final int N_PIECES = 7;

	public boolean lost = false;
	
	public TLabel label;
	
	//current turn
	private int turn = 0;
	private int cleared = 0;
	
	//each square in the grid - int means empty - other values mean the turn it was placed
	private int[][] field = new int[ROWS][COLS];
	//top row+1 of each column
	//0 means empty
	private int[] top = new int[COLS];
	
	//number of next piece
	protected int nextPiece;
		
	//all legal moves - first index is piece type - then a list of 2-length arrays
	protected static int[][][] legalMoves = new int[N_PIECES][][];
	
	//indices for legalMoves
	public static final int ORIENT = 0;
	public static final int SLOT = 1;
	
	//possible orientations for a given piece type
	protected static int[] pOrients = {1,2,4,4,4,2,2};
	
	//the next several arrays define the piece vocabulary in detail
	//width of the pieces [piece ID][orientation]
	protected static int[][] pWidth = {
			{2},
			{1,4},
			{2,3,2,3},
			{2,3,2,3},
			{2,3,2,3},
			{3,2},
			{3,2}
	};
	//height of the pieces [piece ID][orientation]
	private static int[][] pHeight = {
			{2},
			{4,1},
			{3,2,3,2},
			{3,2,3,2},
			{3,2,3,2},
			{2,3},
			{2,3}
	};
	private static int[][][] pBottom = {
		{{0,0}},
		{{0},{0,0,0,0}},
		{{0,0},{0,1,1},{2,0},{0,0,0}},
		{{0,0},{0,0,0},{0,2},{1,1,0}},
		{{0,1},{1,0,1},{1,0},{0,0,0}},
		{{0,0,1},{1,0}},
		{{1,0,0},{0,1}}
	};
	private static int[][][] pTop = {
		{{2,2}},
		{{4},{1,1,1,1}},
		{{3,1},{2,2,2},{3,3},{1,1,2}},
		{{1,3},{2,1,1},{3,3},{2,2,2}},
		{{3,2},{2,2,2},{2,3},{1,2,1}},
		{{1,2,2},{3,2}},
		{{2,2,1},{2,3}}
	};
	
	//initialize legalMoves
	{
		//for each piece type
		for(int i = 0; i < N_PIECES; i++) {
			//figure number of legal moves
			int n = 0;
			for(int j = 0; j < pOrients[i]; j++) {
				//number of locations in this orientation
				n += COLS+1-pWidth[i][j];
			}
			//allocate space
			legalMoves[i] = new int[n][2];
			//for each orientation
			n = 0;
			for(int j = 0; j < pOrients[i]; j++) {
				//for each slot
				for(int k = 0; k < COLS+1-pWidth[i][j];k++) {
					legalMoves[i][n][ORIENT] = j;
					legalMoves[i][n][SLOT] = k;
					n++;
				}
			}
		}
	
	}
	
	
	public int[][] getField() {
		return field;
	}

	public int[] getTop() {
		return top;
	}

    public static int[] getpOrients() {
        return pOrients;
    }
    
    public static int[][] getpWidth() {
        return pWidth;
    }

    public static int[][] getpHeight() {
        return pHeight;
    }

    public static int[][][] getpBottom() {
        return pBottom;
    }

    public static int[][][] getpTop() {
        return pTop;
    }

	public int getNextPiece() {
		return nextPiece;
	}
	
	public boolean hasLost() {
		return lost;
	}
	
	public int getRowsCleared() {
		return cleared;
	}
	
	public int getTurnNumber() {
		return turn;
	}
	
	//constructor
	public SimulationState() {
	}

	public void initState(State s)
	{
		int[][] oldField = s.getField(); 
		for(int i=0; i < field.length; i++)
			for(int j=0; j < field[i].length; j++)
				field[i][j] = oldField[i][j];
		this.nextPiece = s.getNextPiece();
		this.top = Arrays.copyOf(s.getTop(), s.getTop().length);
		cleared=0;
	}
	
	//random integer, returns 0-6
	private int randomPiece() {
		return (int)(Math.random()*N_PIECES);
	}
	
	//gives legal moves for 
	public int[][] legalMoves() {
		return legalMoves[nextPiece];
	}
	
	//make a move based on the move index - its order in the legalMoves list
	public void makeMove(int move) {
		makeMove(legalMoves[nextPiece][move]);
	}
	
	//make a move based on an array of orient and slot
	public void makeMove(int[] move) {
		makeMove(move[ORIENT],move[SLOT]);
	}
	
	//returns false if you lose - true otherwise
	public boolean makeMove(int orient, int slot) {
		//height if the first column makes contact
		int height = top[slot]-pBottom[nextPiece][orient][0];
		//for each column beyond the first in the piece
		for(int c = 1; c < pWidth[nextPiece][orient];c++) {
			height = Math.max(height,top[slot+c]-pBottom[nextPiece][orient][c]);
		}
		
		//check if game ended
		if(height+pHeight[nextPiece][orient] >= ROWS) {
			lost = true;
			return false;
		}

		
		//for each column in the piece - fill in the appropriate blocks
		for(int i = 0; i < pWidth[nextPiece][orient]; i++) {
			
			//from bottom to top of brick
			for(int h = height+pBottom[nextPiece][orient][i]; h < height+pTop[nextPiece][orient][i]; h++) {
				field[h][i+slot] = 1;
			}
		}
		
		//adjust top
		for(int c = 0; c < pWidth[nextPiece][orient]; c++) {
			top[slot+c]=height+pTop[nextPiece][orient][c];
		}
		
		int rowsCleared = 0;
		
		//check for full rows - starting at the top
		for(int r = height+pHeight[nextPiece][orient]-1; r >= height; r--) {
			//check all columns in the row
			boolean full = true;
			for(int c = 0; c < COLS; c++) {
				if(field[r][c] == 0) {
					full = false;
					break;
				}
			}
			//if the row was full - remove it and slide above stuff down
			if(full) {
				rowsCleared++;
				cleared++;
				//for each column
				for(int c = 0; c < COLS; c++) {

					//slide down all bricks
					for(int i = r; i < top[c]; i++) {
						field[i][c] = field[i+1][c];
					}
					//lower the top
					top[c]--;
					while(top[c]>=1 && field[top[c]-1][c]==0)	top[c]--;
				}
			}
		}
		
		return true;
	}
	
	public void draw() {
		label.clear();
		label.setPenRadius();
		//outline board
		label.line(0, 0, 0, ROWS+5);
		label.line(COLS, 0, COLS, ROWS+5);
		label.line(0, 0, COLS, 0);
		label.line(0, ROWS-1, COLS, ROWS-1);
		
		//show bricks
				
		for(int c = 0; c < COLS; c++) {
			for(int r = 0; r < top[c]; r++) {
				if(field[r][c] != 0) {
					drawBrick(c,r);
				}
			}
		}
		
		for(int i = 0; i < COLS; i++) {
			label.setPenColor(Color.red);
			label.line(i, top[i], i+1, top[i]);
			label.setPenColor();
		}
		
		label.show();
		
		
	}
	
	public static final Color brickCol = Color.gray; 
	
	private void drawBrick(int c, int r) {
		label.filledRectangleLL(c, r, 1, 1, brickCol);
		label.rectangleLL(c, r, 1, 1);
	}
	
	public void drawNext(int slot, int orient) {
		for(int i = 0; i < pWidth[nextPiece][orient]; i++) {
			for(int j = pBottom[nextPiece][orient][i]; j <pTop[nextPiece][orient][i]; j++) {
				drawBrick(i+slot, j+ROWS+1);
			}
		}
		label.show();
	}
	
	//visualization
	//clears the area where the next piece is shown (top)
	public void clearNext() {
		label.filledRectangleLL(0, ROWS+.9, COLS, 4.2, TLabel.DEFAULT_CLEAR_COLOR);
		label.line(0, 0, 0, ROWS+5);
		label.line(COLS, 0, COLS, ROWS+5);
	}
}


/* 
 * Abstract class for features to extend.
 * A feature is something that the agent considers when deciding on which move to
 * make. Please refer to the report for a more detailed description on
 * how our agent makes use of features.
 * 
 */
abstract class Feature
{	
	String name;
	int weight;

	public Feature(int w, String n)
	{
		this.weight = w;
		this.name = n;
	}

	public void setWeight(int w)
	{
		this.weight = w;
	}

	int calcScore(State s, SimulationState ss, int orient, int slot)
	{
		return execute(s, ss, orient, slot) * weight;
	}

	abstract int execute(State s, SimulationState ss, int orient, int slot);

	public String toString()
	{
		return name + ": " + weight;
	}
}

//Height feature does not take cleared lines in consideration
class HeightFeature extends Feature
{
	public HeightFeature(int weight)
	{
		super(weight, "Height");
	}

	// Returns the new height if putting the current piece in the specified 
	// slot with the specified orientation
	int execute(State s, SimulationState ss, int orient, int slot)
	{
		int nextPiece = s.getNextPiece(); // the piece in play
		int top[] = s.getTop();

		// Properties of the pieces in the tetris game
		int pBottom[][][] = State.getpBottom();
		int pWidth[][] = State.getpWidth();

		// The maximum height
		int height = 0;

		// Calculate the maximum height for each column that is affected
		// by the current piece
		for (int c = 0; c < pWidth[nextPiece][orient]; c++)
			height = Math.max(height, top[slot + c] - pBottom[nextPiece][orient][c]);

		return height < 4 ? 1 : height;
	}
}

// Checks the number of walls (outer wall or previous blocks) that the current
// piece will touch if placed with the specified slot and orientation
class TouchingWallsFeature extends Feature
{
	public TouchingWallsFeature(int weight)
	{
		super(weight, "TouchingWalls");
	}

	int execute(State s, SimulationState ss, int orient, int slot)
	{
		int nextPiece = s.getNextPiece();

		// Piece inforrmation
		int pWidth = State.getpWidth()[nextPiece][orient];
		int pBottom[] = State.getpBottom()[nextPiece][orient];
		int pTop[] = State.getpTop()[nextPiece][orient];

		// Field information
		int curField[][] = s.getField();
		int top[] = s.getTop();

		int placingRow = 0; // The smallest index of the row that the current piece will touch
		for (int i = 0; i < pWidth; i++)
			placingRow = Math.max(placingRow, top[slot + i] - pBottom[i]);          

		int count = 0; // the number of touching walls
		for (int c = 0; c < pWidth; c++)
			for (int r = pBottom[c] + placingRow; r < pTop[c] + placingRow; r++)
			{
				// the brick we check now will end up at index (r, slot + c) in the field
				// Check for out of bounds error (if we place the piece here we will DIE)
				if (r >= State.ROWS)
					return 0;

				// Count walls to the left
				if (slot + c == 0 || curField[r][slot + c - 1] != 0)
					count+=2;

				// Check walls to the right
				if (slot + c == State.COLS - 1 || curField[r][slot + c + 1] != 0)
					count+=2;

				// Count walls below
				if (r == 0 || curField[r - 1][slot + c] != 0)
					count+=3;
			}
		return count;
	}
}


//Checks how many lines that were removed
class LineRemoveFeatures extends Feature
{

	public LineRemoveFeatures(int weight)
	{
		super(weight, "LinesRemoved");
	}

	int execute(State s, SimulationState ss, int orient, int slot)
	{
		return ss.getRowsCleared();
	}
}

//Searches from top and checks if there are any empty blocks under another block
class HoleCheckFeature extends Feature
{
	public HoleCheckFeature(int weight)
	{
		super(weight, "HoleChecker");
	}

	int execute(State s, SimulationState ss, int orient, int slot)
	{
		//State information
		int[] top = ss.getTop();
		int[][] field = ss.getField();

		//Start with the leftmost column, start from the top and go down and look for any holes.
		int holes = 0;
		for(int i = 0; i < State.COLS; i++)
		{
			if(top[i] == 0) continue; //If the top is in the bottom we dont have to search for any holes
			for(int j = top[i]-1; j >= 0; j--)
				if(field[j][i] == 0)
					holes++;
		}
		return holes;
	}
}

//Searches top and checks if there are good places to put the s-brick, l-brick and square-brick 
class GoodTopFeature extends Feature
{

	public GoodTopFeature(int weight)
	{
		super(weight, "GoodTop");
	}

	int execute(State s, SimulationState ss, int orient, int slot)
	{
		int[] top = ss.getTop();
		int points = 0;
		boolean square = false, lbrick = false, sbrick = false;

		//Loop through all columns
		for(int i = 0; i < State.COLS-1; i++)
		{
			//Checks for two consecutive bricks on the same level
			if(!square && top[i] == top[i+1])
			{
				square = true;
				points++;
			}
			//Checks for a height difference of two between two blocks or three blocks where the first is one lower than the two others
			else if(!lbrick && (top[i]+2 == top[i+1]) ||
					(i < State.COLS-2 && top[i] == top[i+1]+1 && top[i] == top[i+2]+1 )) 
			{
				lbrick = true;
				points++;
			}
			//Checks for a height difference of one between two blocks
			else if(!sbrick && top[i]+1 == top[i+1] || top[i]-1 == top[i+1])
			{
				sbrick = true;
				points += 1;
			}
		}
		return points;
	}
}


/* The player class that uses the features and has the pickMove function.
 * Starting this in a thread will run a game. To get the result from that game
 * you have to set an observer with setObserver(HillClimber).
 */
public class PlayerSkeleton
{
	private ArrayList<Feature> features = new ArrayList<Feature>();

	private static final int SPEED = 10; // the lower the faster (ms between pieces)
	private static int HEIGHT_WEIGHT = -18;
	private static int TOUCHING_WEIGHT = 38;
	private static int LINESREMOVED_WEIGHT = 27;
	private static int HOLECHECKER_WEIGHT = -53;
	private static int GOODTOP_WEIGHT = 17;

	private final SimulationState ss = new SimulationState();

	public PlayerSkeleton()
	{
		features.add(new HeightFeature(HEIGHT_WEIGHT));
		features.add(new TouchingWallsFeature(TOUCHING_WEIGHT));
		features.add(new LineRemoveFeatures(LINESREMOVED_WEIGHT));
		features.add(new HoleCheckFeature(HOLECHECKER_WEIGHT));
		features.add(new GoodTopFeature(GOODTOP_WEIGHT));
	}

	private int calcMoveScore(State s, SimulationState ss, int orient, int slot)
	{
		int totalScore = 0;

		for(Feature f : features)
			totalScore += f.calcScore(s, ss, orient, slot);

		return totalScore;
	}

	// Returns which move to pick based on the current playing state
	// and the array of available moves.
	public int pickMove(State s, int[][] legalMoves) 
	{       
		int score = -Integer.MAX_VALUE; // We want to pick the move with the maximum score

		int bestMove = 0;
		for (int i = 0; i < legalMoves.length; i++)
		{
			ss.initState(s);
			boolean alive = ss.makeMove(legalMoves[i][0], legalMoves[i][1]);
			if(!alive) continue;

			int curScore = calcMoveScore(s, ss, legalMoves[i][0], legalMoves[i][1]);
			if (curScore > score)
			{
				score = curScore;
				bestMove = i;
			}
		}
		return bestMove;
	}

	public ArrayList<Feature> getFeatures()
	{
		return features;
	}

	public static void main(String[] args) 
	{
		PlayerSkeleton p = new PlayerSkeleton();
		State s = new State();
		new TFrame(s);
		while(!s.hasLost())
		{
			s.makeMove(p.pickMove(s,s.legalMoves()));
			{
				s.draw();
				s.drawNext(0,0);
				try {
					Thread.sleep(SPEED);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		int resScore = s.getRowsCleared(); 
		System.out.println("You scored " + resScore + " lines cleared."); 
	}
}
